﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Text;
using System.Windows.Media;
using System.Threading;
using System.IO;
using System.Net.Browser;
using Agent.Messaging;
using NetworkInterface = Microsoft.Phone.Net.NetworkInformation.NetworkInterface;
using Microsoft.Phone.BackgroundAudio;

namespace WindowsPhoneAudioStreaming.Audio.Streaming {

    public enum BufferStatus {
        Opening,
        NotBuffering,
        BufferEmpty,
        Buffering
    }


    public class BufferedMediaStreamSource : RawMP3MediaStreamSource {
        public static ManualResetEvent StreamLock = new ManualResetEvent(true);

        /// <summary>
        /// The amount of time to wait when attempting to read from the http
        /// stream
        /// </summary>
        private const int MaximumHttpReadTime = 10000;
        private const int MaximumNetworkConnectionWait = 15000;
        private const int SleepOnNetworkNone = 15000;

        /// <summary>
        /// The amount of time that playback can be stopped for before the
        /// http stream is cancelled
        /// </summary>
        private const int MaximumPlaybackPauseTime = 1000;

        /// <summary>
        /// The amount of data to read from the http stream
        /// </summary>
        private const int ReadBufferSize = 8192;


        /// <summary>
        /// The url to open for http read. This is the radio stream url
        /// and a random value will be appended to ensure no caching
        /// </summary>
        private readonly Uri uri;

        /// <summary>
        /// The buffered stream (cyclic) that data will be written to
        /// and then frames read from
        /// </summary>
        internal BufferedStream BufferedStream = new BufferedStream();

        /// <summary>
        /// The web request
        /// </summary>
        private HttpWebRequest webRequest;

        /// <summary>
        /// The web response
        /// </summary>
        private HttpWebResponse webResponse;

        /// <summary>
        /// The response stream to read from
        /// </summary>
        private Stream httpResponseStream;

        /// <summary>
        /// Indicates whether an exception has been raised
        /// reading from the last web response
        /// </summary>
        private static bool networkIssue = false;

        /// <summary>
        /// Indicates whether streaming should be wifi only
        /// </summary>
        //private static bool wifiStreamingOnly;
        private const int MaxRetryCount = 6;

        private static Microsoft.Phone.Net.NetworkInformation.NetworkInterfaceType ni = Microsoft.Phone.Net.NetworkInformation.NetworkInterfaceType.None;
        DownloadTracker tracker;
        //static TraceLogger tracer;        

        /// <summary>
        /// Reset event for detecting when network connectivity changes
        /// </summary>
        static private readonly AutoResetEvent ReconnectWaiter = new AutoResetEvent(false);

        /// <summary>
        /// The separation between icy in-stream meta data (0 if no meta data)
        /// </summary>
        private int metaBlockSeparation;

        private readonly AutoResetEvent startingRead = new AutoResetEvent(false);
        private readonly AutoResetEvent readingCompleted = new AutoResetEvent(false);
        private readonly AutoResetEvent httpReadMonitor = new AutoResetEvent(false);
        private MessageReceiver messageReceiver;

        /// <summary>
        /// Creates and initializes a new instance of the BufferedMediaStreamSource
        /// </summary>
        /// <param name="uri">The uri of the radio stream</param>
        /// <param name="options">Playback options</param>
        public BufferedMediaStreamSource(Uri uri, IPlayerOptions optionsGenerator)
            : base(optionsGenerator)
        {
            this.uri = uri;
        }

        /// <summary>
        /// Sets up the buffered stream and kicks of the radio stream
        /// by triggering the http request
        /// </summary>
        protected override void OpenMediaAsync()
        {
            options = LoadOptions();
            //wifiStreamingOnly = options.WiFiAndEthernetOnly;
            tracker = new DownloadTracker(options.LogUpdateInSeconds, options.CellularStreamingThreshold);

            TraceLogger.TraceInitialise();

            //networkIssue = true;
            NetworkChange.NetworkAddressChanged += OnNetworkChange;

            // Setup the underlying stream source to use
            // the buffered stream
            InitializeFromStream(BufferedStream);

            // Initiate the reading from http request
            InitiateWebRequest();

            messageReceiver = new MessageReceiver("AudioAgent");
            messageReceiver.MessageReceived += MessageReceived;
            messageReceiver.Stopped += Stopped;
            messageReceiver.Start();


            base.OpenMediaAsync();
        }

        private void Stopped(object sender, EventArgs e)
        {
            
        }

        private void MessageReceived(object sender, MessageReceivedEventArgs e)
        {
            tracker.Flush();
        }


        /// <summary>
        /// Initiate the http request
        /// </summary>
        private void InitiateWebRequest() {
            // Clean up any existing objects
            CleanupExistingRequestAndResponseObjects();

            // validate there is a network connection, if wifi only then wait till connected via wifi
            ValidateNetworkConnection();

            // Make sure the underlying buffered stream is flushed
            BufferedStream.Flush();

            // StreamLock.WaitOne();

            // Launch the web request
            StartWebRequest();
        }

        /// <summary>
        /// Creates and launches the web request
        /// </summary>
        private void StartWebRequest() {
            // Create the url by adding a random query parameter
            var wuri = new UriBuilder(uri) {
                Query = Guid.NewGuid().ToString()
            };
            var weburi = wuri.Uri;

            // Create the web request
            webRequest = (HttpWebRequest)WebRequestCreator.ClientHttp.Create(weburi);
            webRequest.Accept = ("*/*");
            webRequest.AllowReadStreamBuffering = (false);
            webRequest.AllowAutoRedirect = (true);

            //var options = LoadOptions();
            if (options.EnableIcyMetaData) {
                webRequest.Headers["Icy-MetaData"] = "1";
            }

            // Kick of the web request
            webRequest.BeginGetResponse(HttpResponseCallback, webRequest);
        }


        static void OnNetworkChange(object sender, EventArgs e) {
            TraceLogger.WriteTrace("Network change event");

            networkIssue = true;
            if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable()) {
                ReconnectWaiter.Set();
            }
        }


        /// <summary>
        /// Checks that there is a valid internet connection. If not, this method
        /// will block until a connection is available
        /// </summary>
        private void ValidateNetworkConnection() {
            int retry = 0;
            networkIssue = true;
            BufferedStream.ConnectionState(BufferedStream.ConnectionStatus.NotConnected);
            do {
                if (retry > MaxRetryCount) {
                    retry = 0;
                    StreamLock.Reset();
                    BackgroundAudioPlayer.Instance.Stop();
                }

                // Check to see if the stream has been locked (by pause/stop operation on the player)
                StreamLock.WaitOne();
                TraceLogger.WriteTrace("StreamLock.WaitOne released");


                // reload options, wifi only etc may have changed
                options = LoadOptions();

                if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable()) {

                    ni = NetworkInterface.NetworkInterfaceType;

                    switch (ni) {
                        case Microsoft.Phone.Net.NetworkInformation.NetworkInterfaceType.MobileBroadbandCdma:
                        case Microsoft.Phone.Net.NetworkInformation.NetworkInterfaceType.MobileBroadbandGsm:
                            if (options.WiFiAndEthernetOnly) {ReconnectWaiter.WaitOne(MaximumNetworkConnectionWait);}
                            else {networkIssue = false;}
                            break;
                        case Microsoft.Phone.Net.NetworkInformation.NetworkInterfaceType.Wireless80211:
                        case Microsoft.Phone.Net.NetworkInformation.NetworkInterfaceType.Ethernet:
                            networkIssue = false;
                            break;
                        case Microsoft.Phone.Net.NetworkInformation.NetworkInterfaceType.None:
                        default:
                            retry++;
                            // when network interface is none for some reason a network changed event does not fire so sleep instead
                            Thread.Sleep(SleepOnNetworkNone);
                            break;
                    }
                }
                else {
                    retry++;
                    ReconnectWaiter.WaitOne(MaximumNetworkConnectionWait);
                }
            }
            while (networkIssue);
            BufferedStream.ConnectionState(BufferedStream.ConnectionStatus.Connected);
            TraceLogger.WriteTrace("Validated on: " + ni.ToString());
        }


        /// <summary>
        /// The response call back for the web request
        /// </summary>
        /// <param name="asynchronousResult"></param>
        private void HttpResponseCallback(IAsyncResult asynchronousResult) {
            TraceLogger.WriteTrace("Web request completed");
            var httpWebRequest = (HttpWebRequest)asynchronousResult.AsyncState;
            try {
                // Get the web response
                webResponse = (HttpWebResponse)httpWebRequest.EndGetResponse(asynchronousResult);
                // Get the response stream
                httpResponseStream = webResponse.GetResponseStream();
            }
            catch (Exception ex) {
                Debug.WriteLine(ex.Message);
                TraceLogger.WriteTrace("Web exception:" + ex.Message);
                networkIssue = true;
            }

            try {
                if (webResponse != null &&
                    httpResponseStream != null &&
                    !networkIssue) {
                    // Retrieve header data (including icy meta data headers
                    // that may be in the body of the response)
                    int position = RetrieveHeaderData();

                    // We're just starting the read from the http response.
                    // To ensure we don't have duplicated buffers etc, we need 
                    // to flush and reset the output buffer
                    BufferedStream.Flush();

                    // Start the Http Monitor (and wait for it to start)
                    ThreadPool.QueueUserWorkItem(HttpMonitor);
                    httpReadMonitor.WaitOne();

                    try {
                        ReadResponseStreamToBufferedStream(position);
                    }
                    catch (Exception ex) {
                        TraceLogger.WriteTrace("process stream:" + ex.Message);
                    }
                    finally {
                        // Make sure request/response objects are cleaned up
                        // and that the http monito has stopped
                        CleanupExistingRequestAndResponseObjects();
                        httpReadMonitor.WaitOne();
                    }
                }
            }
            catch (Exception ex) {
                networkIssue = true;
                Debug.WriteLine(ex.Message);
            }

            // Initiate a new web request
            InitiateWebRequest();
        }



        private int RetrieveHeaderData()
        {
            //var options = LoadOptions();
            if (options.EnableIcyMetaData) {
                // Shoutcast can return ICE 200 instead of HTTP 200 OK which means that
                // the headers will appear as part of the response body instead of being
                // read off as headers by Windows Phone
                var headersFound = webResponse.Headers.Count > 0;
                var headerDictionary = new Dictionary<string, string>();

                if (headersFound) {
                    // Extract the Icy headers from the actual response headers
                    ExtractHeaders(headerDictionary);
                }


                // Now let's see whether there is any ICY headers in the body
                if (!headersFound) {
                    return ExtractHeadersFromBody(headerDictionary);
                }

                // Extract any values that are going to be used reading data
                // from the radio stream (eg meta data separation for icy meta data)
                ProcessHeaders(headerDictionary);
            }

            return 0;
        }

        private void ReadResponseStreamToBufferedStream(int position) {
            //TraceLogger.WriteTrace("ResponseToBufferedStream begin");
            // Create the read array
            var array = new byte[ReadBufferSize];
            int num = 0;
            do {
                // Normally the stream lock wouldn't have been set so
                // this would fall through immediately. However, if playback
                // has been stopped we'll wait here until it resumes. At 
                // which point it is more than likely the request will
                // fail and have to restart.
                if (!StreamLock.WaitOne(MaximumPlaybackPauseTime)) {
                    break;
                }

                // Read the next chunk of data
                Debug.WriteLine("num before: " + num + " " + position);
                num = MonitoredHttpRead(array);

                Debug.WriteLine("num after : " + num + " " + position);


                // Detect if there is meta data in the read data
                if (metaBlockSeparation > 0 &&
                    (position + num) > metaBlockSeparation) {
                    // Extract the meta data and update the read position
                    // Any excess audio data is also written to the buffered
                    // stream
                    position = ExtractMetaData(array, position);
                }
                else {
                    // Update read position and write data to buffered stream
                    position += num;
                    BufferedStream.Write(array, 0, num);
                }

                // If no data raised, assume that the response has been terminated
                // perhaps from lack of network connection. Set the webExceptionRaised
                // flag to trigger connection detection logic
                if (num == 0) {
                    networkIssue = true;
                }

            } while (num != 0 && !networkIssue);
            //TraceLogger.WriteTrace("ResponseToBufferedStream end");
        }

        private int ExtractMetaData(byte[] array, int position) {
            // Arraypos is the position in the array of data 
            // read from the http stream
            // Write up to the meta data to the output stream
            int arraypos = metaBlockSeparation - position;
            BufferedStream.Write(array, 0, arraypos);

            // Determine the amount of meta data
            var metalength = array[arraypos] * 16;
            var metaDataArray = new byte[metalength];
            arraypos++;

            // Determine how much data is left in the http data array
            // and determine if all the meta data can be read from this
            // array, or if more data needs to be read from http stream
            var diff = array.Length - arraypos;
            var amountToReadFromHttpArray = Math.Min(metaDataArray.Length, diff);
            // Read the data from the http array
            Array.Copy(array, arraypos, metaDataArray, 0, amountToReadFromHttpArray);
            arraypos += amountToReadFromHttpArray;
            // Now work out if we need to read more http data
            if (amountToReadFromHttpArray < metaDataArray.Length) {
                // Not enough data read, so read the rest from the http stream
                MonitoredHttpRead(metaDataArray, amountToReadFromHttpArray,
                                        metaDataArray.Length - amountToReadFromHttpArray);
                position = 0;
            }
            else {
                // All meta data read from current http array, which 
                // probably means there is more data left in the array.
                // this needs to be written to the output stream
                position = array.Length - arraypos;
                BufferedStream.Write(array, arraypos, position);
            }

            // Trim out the null string character
            const byte nullChar = (byte)'\0';
            for (int i = 0; i < metaDataArray.Length; i++) {
                if (metaDataArray[i] == nullChar) {
                    metalength = i - 1;
                    break;
                }
            }

            // Extract the string
            var str = Encoding.UTF8.GetString(metaDataArray, 0,
                                              metalength);
            if (str.Length > 0) {
                Debug.WriteLine(str);
                if (str.ToCharArray().Any(c => !(char.IsLetterOrDigit(c)
                                                 || char.IsPunctuation(c)
                                                 || c == ' '
                                                 || c == '='))) {
                    throw new Exception("Error with this stream");
                }
                var pieces = str.Split('\'', '\"');

                if (pieces.Length >= 2) {
                    var title = pieces[1].Trim();

                    Debug.WriteLine(title);
                }

            }
            return position;
        }

        /// <summary>
        /// Ensures the http read doesn't lock up - the http read monitor
        /// will kick in to terminate the response if read takes too long
        /// </summary>
        /// <param name="array"></param>
        /// <returns></returns>
        private int MonitoredHttpRead(byte[] array) {
            return MonitoredHttpRead(array, 0, array.Length);
        }

        private int MonitoredHttpRead(byte[] array, int start, int length) {
            startingRead.Set();
            try {

                var read = httpResponseStream.Read(array, start, length);
                tracker.UpdateBytesDownloaded(read, ni);
                return read;
            }
            finally {
                readingCompleted.Set();
            }
        }

        /// <summary>
        /// This monitors the http read to check that it happens in 
        /// a suitable time frame. If not, it aborts the request/response
        /// forcing it to restart
        /// </summary>
        /// <param name="state"></param>
        private void HttpMonitor(object state) {
            try {
                // Indicate that the read monitor has started
                httpReadMonitor.Set();
                do {
                    // Wait for read to start. Limit the wait
                    // time just in case the reading has ended
                    // ie completed=true
                    if (startingRead.WaitOne(5000)) {
                        // Check to make sure read happens in an appropriate
                        // time frame
                        if (!readingCompleted.WaitOne(MaximumHttpReadTime)) {
                            // Forcibly dispose of the response stream
                            // which will prevent any further reading
                            CleanupExistingRequestAndResponseObjects();
                        }
                    }
                } while (httpResponseStream != null);

                // Indicate that the read monitor has ended
                httpReadMonitor.Set();
            }
            catch (Exception ex) {
                Debug.WriteLine(ex.Message);
            }

        }

        private void ProcessHeaders(Dictionary<string, string> headerDictionary) {
            foreach (var icyHeader in headerDictionary) {
                switch (icyHeader.Key) {
                    case "icy-metaint":
                        metaBlockSeparation = int.Parse(icyHeader.Value);
                        Debug.WriteLine("Separation: " + metaBlockSeparation);
                        break;
                }
            }
        }

        private int ExtractHeadersFromBody(Dictionary<string, string> headerDictionary) {
            int position;
            var headerReadPosition = 0;
            var headerReader = new MyStream(httpResponseStream);
            var responseLine = headerReader.ReadLine();
            if (responseLine != null && responseLine.Trim() == "ICY 200 OK") {
                headerReadPosition +=
                    headerReader.CurrentEncoding.GetBytes(responseLine + Environment.NewLine.Length).Length;

                bool headerIsDone = false;
                while (!headerIsDone) {
                    string headerLine = headerReader.ReadLine() + "";
                    if (headerLine.Equals("")) {
                        headerIsDone = true;
                    }
                    else {
                        var bits = headerLine.Split(':');
                        if (bits.Length == 2) {
                            var key = bits[0].ToLower();
                            if (IsIcyKey(key)) {
                                headerDictionary[key] = bits[1];
                            }
                        }
                    }

                    headerReadPosition +=
                        headerReader.CurrentEncoding.GetBytes(headerLine + Environment.NewLine.Length).Length;
                }
                position = (int)httpResponseStream.Position - (int)headerReader.Pos;
                Debug.WriteLine("Position: " + position + " Tracker: " + headerReadPosition + " pos: " + headerReader.Pos);
            }
            else {
                // Update the position tracking as if it were reading
                // from beginning of the stream.
                position = (int)httpResponseStream.Position;
            }
            return position;
        }

        private void ExtractHeaders(Dictionary<string, string> headerDictionary) {
            // Extract icy headers
            foreach (var headerKey in webResponse.Headers.AllKeys) {
                var key = headerKey.ToLower();
                var val = webResponse.Headers[key];
                if (IsIcyKey(key)) {
                    if (key == "ice-audio-info") {

                        var pairs = val.Split(';');
                        foreach (var p in pairs) {
                            var bits = p.Split('=');
                            if (bits.Length == 2) {
                                headerDictionary[bits[0]] = bits[1];
                            }
                        }
                    }
                    else {
                        headerDictionary[key] = webResponse.Headers[key];
                    }
                }
            }
        }

        private static bool IsIcyKey(string key) {
            return key != null && (key.StartsWith("icy") || key.StartsWith("ice"));
        }




        #region Cleanup
        /// <summary>
        /// Ensures any existing request/response objects
        /// are appropriately cleaned up
        /// </summary>
        private void CleanupExistingRequestAndResponseObjects() {
            CleanupResponseStream();
            CleanupResponseObject();
            CleanupRequestObject();
        }

        private void CleanupRequestObject() {
            try {
                if (webRequest != null) {
                    webRequest.Abort();
                    webRequest = null;
                }
            }
            catch (Exception ex) {
                Debug.WriteLine(ex.Message);
            }
        }

        private void CleanupResponseObject() {
            try {
                if (webResponse != null) {
                    webResponse.Close();
                    webResponse.Dispose();
                    webResponse = null;
                }
            }
            catch (Exception ex) {
                Debug.WriteLine(ex.Message);
            }
        }

        private void CleanupResponseStream() {
            try {
                if (httpResponseStream != null) {
                    httpResponseStream.Close();
                    httpResponseStream.Dispose();
                    httpResponseStream = null;
                }
            }
            catch (Exception ex) {
                Debug.WriteLine(ex.Message);
            }
        }

        #endregion

        protected override void CloseMedia() {
            // Clean up the request and response objects
            CleanupExistingRequestAndResponseObjects();

            NetworkChange.NetworkAddressChanged -= OnNetworkChange;


            base.CloseMedia();
        }

        protected override void SwitchMediaStreamAsync(MediaStreamDescription mediaStreamDescription) {
        }

        protected override void ChangeBufferLength(int lengthInBytes) {
            base.ChangeBufferLength(lengthInBytes);

            BufferedStream.UpperThreshold = lengthInBytes;
            //BufferedStream.FillingBuffer = true;
            BufferedStream.BufferingEnabled = true;
            BufferedStream.FillToUpperThreshold = true;
        }

    }

    public class MyStream : StreamReader {
        private uint pos;

        public MyStream(Stream strm)
            : base(strm) {
        }

        public override string ReadLine() {
            int i;
            string line = null;
            int eolCount = 0;
            while ((i = base.Read()) != -1) {
                var current = (char)i;
                pos++;

                if (IsFeed(current)) {
                    eolCount++;
                    if (eolCount >= 2) break;
                    if ((i = base.Peek()) != -1) {
                        if (!IsFeed((char)i))
                            break;
                    }
                }
                else line += current;
            }

            return line + "";
        }

        private bool IsFeed(char c) {
            if (c == '\r' || c == '\n')
                return true;

            return false;
        }

        public uint Pos {
            get { return pos; }
        }
    }


}
